package baidu_netdisk

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/chicken-team-outside/chicken_transmission/api_messages"
	"github.com/chicken-team-outside/chicken_transmission/driver"
	bdclient "github.com/chicken-team-outside/chicken_transmission/driver/baidu/openxpanapi"
	"github.com/chicken-team-outside/chicken_transmission/model"
	"github.com/pkg/errors"
	"go.uber.org/zap"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"sync"
	"sync/atomic"
)

var apiClient atomic.Value

func getApiClient() *bdclient.APIClient {
	return apiClient.Load().(*bdclient.APIClient)
}

func init() {
	configuration := bdclient.NewConfiguration()
	apiClient.Store(bdclient.NewAPIClient(configuration))
}

func Create(accessToken string) *BaiduNetDiskDriver {
	client := &BaiduNetDiskDriver{}
	client.accessToken.Store(accessToken)
	client.Ctx, client.cancel = context.WithCancel(context.Background())
	return client
}

type BaiduNetDiskDriver struct {
	accessToken atomic.Value
	Ctx         context.Context
	cancel      context.CancelFunc
	uniqueLock  sync.Mutex
}

func (B *BaiduNetDiskDriver) GetFileDownloadLink(ctx context.Context, destPath string) (*api_messages.LinkInfo, error) {
	id, err := B.GetFileInfo(ctx, destPath)
	if err != nil {
		return nil, err
	}
	link, err := B.getDLink(ctx, id.Fsid)
	if err != nil {
		return nil, err
	}
	return &api_messages.LinkInfo{
		Link: link + "&" + "access_token=" + B.GetAccessToken(),
		Headers: map[string]string{
			"User-Agent": "pan.baidu.com",
		},
	}, nil
}

func (B *BaiduNetDiskDriver) Exists(ctx context.Context, path string) (bool, error) {
	_, err := B.GetFileInfo(ctx, path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

func (B *BaiduNetDiskDriver) SetAccessToken(accessToken string) {
	B.accessToken.Store(accessToken)
}

func (B *BaiduNetDiskDriver) Remove(ctx context.Context, targetPath string) error {
	targetPath = formatPath(targetPath)
	fileList := []map[string]string{
		{
			"path": targetPath,
		},
	}
	marshal, err := json.Marshal(fileList)
	if err != nil {
		return err
	}
	_, err = getApiClient().FilemanagerApi.Filemanagerdelete(ctx).AccessToken(B.GetAccessToken()).Filelist(string(marshal)).Async(0).Execute()
	return err
}

func (B *BaiduNetDiskDriver) getDLink(ctx context.Context, fsId uint64) (string, error) {
	_, r, err := getApiClient().MultimediafileApi.Xpanmultimediafilemetas(ctx).
		AccessToken(B.GetAccessToken()).Fsids(fmt.Sprintf("[%d]", fsId)).Dlink("1").Execute()
	if err != nil {
		return "", err
	}

	if r.StatusCode != http.StatusOK {
		return "", fmt.Errorf("get dlink failed")
	}
	defer r.Body.Close()

	defer r.Body.Close()
	ret := FileMetasReturn{}
	if err = json.NewDecoder(r.Body).Decode(&ret); err != nil {
		return "", fmt.Errorf("get dlink failed, %v", err)
	}

	if ret.Errno != 0 {
		return "", fmt.Errorf("get dlink failed, errno: %d, errmsg: %s", ret.Errno, ret.Errmsg)
	}
	if len(ret.List) == 0 {
		return "", fmt.Errorf("server returned empty download link list")
	}
	return ret.List[0].Dlink, nil
}

func (B *BaiduNetDiskDriver) GetAccessToken() string {
	if val := B.accessToken.Load(); val != nil {
		return val.(string)
	}
	return ""
}

func (B *BaiduNetDiskDriver) GetQuota(ctx context.Context) (used, total int64, err error) {
	expire := int32(1) // int32 |  (optional)
	free := int32(1)   // int32 |  (optional)
	resp, _, err := getApiClient().UserinfoApi.Apiquota(ctx).AccessToken(B.GetAccessToken()).Checkexpire(expire).Checkfree(free).Execute()
	if err != nil {
		return 0, 0, err
	}
	used = resp.GetUsed()
	total = resp.GetTotal()
	return
}

func (B *BaiduNetDiskDriver) createDirIsNotExist(ctx context.Context, path string) error {
	B.uniqueLock.Lock()
	defer B.uniqueLock.Unlock()
	exists, err := B.Exists(ctx, formatPath(path))
	if err != nil {
		return err
	}
	if !exists {
		return B.MkdirAll(ctx, path)
	}
	return nil
}

func (B *BaiduNetDiskDriver) UploadFile(ctx context.Context, size int64, destPath string, file io.ReadSeeker) error {
	var blockSize int
	userType := B.GetUserType(ctx)
	switch userType {
	case 0:
		blockSize = 4 * 1024 * 1024
	case 1:
		blockSize = 16 * 1024 * 1024
	case 2:
		blockSize = 32 * 1024 * 1024
	default:
		blockSize = 4 * 1024 * 1024
	}
	blt, contentMd5, sliceMd5, err := countBlockMD5List(ctx, size, file, blockSize)
	if err != nil {
		return err
	}
	// precreate
	marshalBlockListStr, err := json.Marshal(blt)
	if err != nil {
		return err
	}
	rType := int32(3)
	destPath = formatPath(destPath)
	preRsp := getApiClient().FileuploadApi.Xpanfileprecreate(ctx).
		AccessToken(B.GetAccessToken()).Path(destPath).Isdir(0).BlockList(string(marshalBlockListStr)).
		Size(size).Autoinit(1).Rtype(rType)
	if sliceMd5 != "" {
		preRsp = preRsp.SliceMd5(sliceMd5).ContentMd5(contentMd5)
	}
	pre, _, err := preRsp.Execute()
	if err != nil {
		return err
	}
	if pre.GetErrno() != 0 {
		return fmt.Errorf("precreate failed, errno : %d", pre.Errno)
	}
	buf := make([]byte, blockSize)
	cloudMd5s := make([]string, 0, len(pre.GetBlockList()))
	for _, uploadIndex := range pre.GetBlockList() {
		siv := strconv.Itoa(uploadIndex)
		readN, err := getFileBlock(file, uploadIndex, blockSize, buf)
		if err != nil {
			return err
		}
		cloudMd5Rsp, _, err := getApiClient().FileuploadApi.Pcssuperfile2(ctx).
			AccessToken(B.GetAccessToken()).Uploadid(pre.GetUploadid()).Partseq(siv).
			Type_("tmpfile").Partname(fmt.Sprintf("part-%d", uploadIndex)).Path(destPath).Block(buf[:readN]).Execute()
		if err != nil {
			return err
		}
		rsp := &PcsSuperFile2Rsp{}
		if err := json.Unmarshal([]byte(cloudMd5Rsp), rsp); err != nil {
			return err
		}
		if rsp.Md5 == "" {
			return fmt.Errorf("pcs superfile2 returns empty md5")
		}
		cloudMd5s = append(cloudMd5s, rsp.Md5)
	}
	marshalBlockListStr, err = json.Marshal(cloudMd5s)
	if err != nil {
		return err
	}
	// create file
	rsp, _, err := getApiClient().FileuploadApi.Xpanfilecreate(ctx).AccessToken(B.GetAccessToken()).
		Path(destPath).Isdir(0).Size(size).Uploadid(pre.GetUploadid()).
		BlockList(string(marshalBlockListStr)).Rtype(rType).Execute()
	if err != nil {
		return err
	}
	return checkErrorCode(&rsp)
}

func (B *BaiduNetDiskDriver) GetFileInfo(ctx context.Context, filePath string) (*FileInfo, error) {
	start := 0      // string |  (optional)
	limit := 100    // int32 |  (optional)
	order := "time" // string |  (optional)
	dirPath := filepath.ToSlash(filepath.Dir(filePath))
	fileName := filepath.Base(filePath)
	for {
		fileList, err := B.listDirHelper(ctx, dirPath, start, limit, true, order)
		if err != nil {
			return nil, err
		}

		for _, v := range fileList {
			if v.ServerFilename == fileName {
				return v, nil
			}
		}
		if len(fileList) < limit {
			break
		} else {
			start++
		}
	}
	return nil, errors.WithMessagef(os.ErrNotExist, "file not found: %s", filePath)
}

func (B *BaiduNetDiskDriver) listDirHelper(ctx context.Context, path string, page, pageSize int, desc bool, order string) ([]*FileInfo, error) {
	query := getApiClient().FileinfoApi.Xpanfilelist(ctx).AccessToken(B.GetAccessToken()).
		Start(strconv.Itoa(page)).Limit(int32(pageSize)).Dir(path).Order(order)
	if desc {
		query = query.Desc(1)
	} else {
		query = query.Desc(0)
	}
	_, r, err := query.Execute()
	if err != nil {
		return nil, err
	}
	defer r.Body.Close()
	if r.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("bad http status code: %d", r.StatusCode)
	}
	ret := FilelistReturn{}
	err = json.NewDecoder(r.Body).Decode(&ret)
	if err != nil {
		return nil, err
	}
	if ret.Errno != 0 {
		return nil, fmt.Errorf("get file id failed, errno: %d", ret.Errno)
	}
	return ret.List, nil
}

func (B *BaiduNetDiskDriver) MkdirAll(ctx context.Context, dirPath string) error {
	rsp, _, err := getApiClient().FileuploadApi.
		Xpanfilecreate(ctx).
		Path(formatPath(dirPath)).
		Isdir(1).
		Size(0).
		Uploadid("").
		AccessToken(B.GetAccessToken()).
		Rtype(0).Execute()
	if err != nil {
		return err
	}
	zap.L().Debug("MkdirAll called", zap.Error(err), zap.Any("response", rsp))
	if rsp.GetErrno() == -8 || rsp.GetErrno() == 0 {
		return nil
	}
	return fmt.Errorf("mkdir all failed, error code %d", rsp.GetErrno())
}

func (B *BaiduNetDiskDriver) GetVendor() model.Vendor {
	return model.Vendor_BaiduNetDisk
}

func (B *BaiduNetDiskDriver) Close() error {
	B.cancel()
	return nil
}

func (B *BaiduNetDiskDriver) ListDir(ctx context.Context, path string) ([]*driver.FileInfo, error) {
	start := 0      // string |  (optional)
	limit := 1000   // int32 |  (optional)
	order := "name" // string |  (optional)
	path = filepath.ToSlash(path)
	var result []*driver.FileInfo
	for {
		fileList, err := B.listDirHelper(ctx, path, start, limit, false, order)
		if err != nil {
			return nil, err
		}
		for _, v := range fileList {
			result = append(result, &driver.FileInfo{
				Name:  v.ServerFilename,
				Size:  int64(v.Size),
				IsDir: v.Isdir == 1,
				Mtime: int64(v.ServerMtime),
			})
		}
		if len(fileList) < limit {
			break
		} else {
			start++
		}
	}
	return result, nil
}

func (B *BaiduNetDiskDriver) GetUserInfo(ctx context.Context) (*bdclient.Uinforesponse, error) {
	info, _, err := getApiClient().UserinfoApi.Xpannasuinfo(ctx).AccessToken(B.GetAccessToken()).Execute()
	if err != nil {
		return nil, err
	}
	return &info, nil
}

func (B *BaiduNetDiskDriver) UserName(ctx context.Context) (string, error) {
	info, err := B.GetUserInfo(ctx)
	if err != nil {
		return "", err
	}
	return info.GetBaiduName(), nil
}

func (B *BaiduNetDiskDriver) GetUserType(ctx context.Context) int32 {
	info, err := B.GetUserInfo(ctx)
	if err != nil {
		zap.L().Error("failed to get user info", zap.Error(err))
		return 0
	}

	return info.GetVipType()
}
